const DDMVariables = require("./ddm");
const Metadata = require("../lib/metadata");
const { VIEWTAG, VIEWNEEDREMOVEPROPS } = require("../lib/const");
const { parseTemplate, parseStyle, excuteStyle, setProp } = require("../lib/helper");
const { StreamManager } = require('./stream');
const { StaticViewGroup } = require('./../view');
const env = require("./env");

class BaseContext {
	constructor(config = {}) {
		this._ddm = new DDMVariables();
		this._config = Object.assign({
			siteURL: '',
			basePath: '',
			name: '',
			version: '',
			apps: [],
			develop: null,
			hooker: '',
			codeMap: {}
		}, config);
		this._isRootContext = this._config.name === 'root';
		this._streamManager = new StreamManager(this);
		this._loader = null;
		this._manager = null;
		this._root = null;
		this._hooks = {
			setcontext() { },
			sourceexcute() { }
		};
	}

	get env() {
		return env;
	}

	get isRootContext() {
		return this._isRootContext;
	}

	get baseURL() {
		if (this.isRootContext) {
			return '/';
		}
		return this._config.siteURL;
	}

	get loader() {
		return this._loader;
	}

	get name() {
		return this._config.name;
	}

	get config() {
		return this._config;
	}

	set config(conf) {
		Object.assign(this._config, conf);
	}

	get ddm() {
		return this._ddm;
	}

	get window() {
	}

	get document() {
	}

	set onsnapshoot(fn) {
		this._onsnapshoot = fn;
	}

	get isBrowser() {
		return true;
	}

	get request() {
		return null;
	}

	get manager() {
		return this._manager;
	}

	_invokeHook(type, params) {
		return Promise.resolve().then(() => this._hooks[type] && this._hooks[type](params));
	}

	_clean(view) {
		this._streamManager.unsubscribe(view);
	}

	_getEntryPath(name) {
		let t = name.split('/');
		let app = t.shift();
		return { app, path: t.join('/') };
	}

	getViewInstance({ viewClass, parent, dom, name = "", useProps = [], id, isRoot, _viewId }) {
		let info = { ...Metadata.getMetadataExtends("view", viewClass.prototype) };
		let ps = Promise.resolve();
		info.className = info.className ? `${this.config.name}-${info.className}` : info.className;
		if (info.template) {
			let code = info.template;
			if (code.path) {
				ps = ps.then(() => this.loader.getSource(info.template.path));
			} else {
				ps = ps.then(() => code);
			}
			ps = ps.then(code => {
				info.template = parseTemplate(code, info.className);
			});
		}
		if (info.style) {
			let code = info.style;
			if (code.path) {
				ps = ps.then(() => this.loader.getSource(info.style.path));
			} else {
				ps = ps.then(() => code);
			}
			ps = ps.then(code => {
				let path = info.module.split('.');
				path.pop();
				path = path.join('.').replace(/\//g, "-");
				excuteStyle(parseStyle(code, info.className), `${this.config.name}-${path}:${info.className}`, this);
			});
		}
		if (isRoot) {
			ps = ps.then(() => {
				return this._invokeHook('DDMIcons').then(icons => {
					(icons || []).forEach(icon => this.ddm.setDefaultIcon(icon));
				}).then(() => {
					return this._invokeHook('DDMFns').then(fns => {
						Reflect.ownKeys(fns || {}).forEach(name => this.ddm.setDefaultFunction(name, fns[name]));
					});
				}).then(() => {
					return this._invokeHook('DDMTags').then(tags => {
						Reflect.ownKeys(tags || {}).forEach(name => this.ddm.setDefaultTag(name, tags[name]));
					});
				}).then(() => {
					return this._invokeHook('DDMMacros').then(macros => {
						Reflect.ownKeys(macros || {}).forEach(name => this.ddm.setDefaultMacro(name, macros[name]));
					});
				}).then(() => {
					return this._invokeHook('DDMDirectives').then(directives => {
						Reflect.ownKeys(directives || {}).forEach(name => this.ddm.setDefaultAssignDirective(name, directives[name]));
					});
				}).then(() => {
					return this._invokeHook('DDMPropMacros').then(propMacros => {
						Reflect.ownKeys(propMacros || {}).forEach(name => this.ddm.setDefaultPropMacro(name, propMacros[name]));
					});
				}).then(() => {
					return this._invokeHook('styles').then(styles => {
						(styles || []).forEach(style => {
							style.active && style.active();
						});
					});
				});
			});
		}
		return ps.then(() => {
			if (info.className) {
				dom.classList.add(info.className);
			}
			let _view = new viewClass({
				parent,
				info,
				name,
				dom,
				useProps,
				context: this,
				id,
				_viewId
			});
			setProp(dom, VIEWTAG, _view);
			return _view;
		}).then(view => {
			return Promise.resolve().then(() => {
				if (isRoot) {
					return this._invokeHook('streams').then(streams => {
						(streams || []).forEach(stream => this._streamManager.set(stream));
					});
				}
			}).then(() => {
				return view;
			});
		});
	}

	getRootView({ rootClass, dom, container, parameter = {}, parent = null, useProps, id = '', _viewId, name, inited }) {
		let rootElement = dom || this.document.createElement("div");
		let removeProps = [];
		if (this.config.name === 'root') {
			rootElement.setAttribute("id", `ada-root-${this.config.name}`);
			rootElement.setAttribute("data-module", `ada-root`);
			removeProps.push('id', 'data-module');
		}
		rootElement.setAttribute("data-app-root", this.config.name);
		removeProps.push('data-app-root');
		if (!dom) {
			container.appendChild(rootElement);
		}
		let root = rootElement[VIEWTAG];
		if (!root || root.isRemoved) {
			return this.getViewInstance({
				viewClass: rootClass,
				parent,
				dom: rootElement,
				name: `ada-root-${this.name}`,
				useProps,
				id,
				_viewId,
				context: this,
				isRoot: true
			}).then(view => {
				view[VIEWNEEDREMOVEPROPS] = removeProps;
				if (inited) {
					return Promise.resolve().then(() => view.oncreated()).then(() => {
						return view.update(parameter).then(() => view.onready());
					}).then(() => {
						this._streamManager.subscribe(view);
					}).then(() => {
						return view;
					});
				} else {
					return view;
				}
			}).then(view => {
				return view;
			});
		}
		return Promise.resolve(root);
	}

	getContextRoot() {
		return this._root;
	}

	getRoot() {
		let t = this.document.querySelector('[data-module="ada-root"]');
		if (t) {
			return t[VIEWTAG];
		}
	}

	getRootContext() {
		let target = this, r = null;
		while (target) {
			if (!target._parent) {
				r = target;
				break;
			} else {
				target = target._parent;
			}
		}
		return r;
	}

	getViewSingleton(viewClass, option) {
		let info = Metadata.getMetadataExtends("view", viewClass.prototype);
		let root = this.getRoot();
		if (!root.context._singleton) {
			root.context._singleton = new Map();
		}
		let singleton = root.context._singleton;
		if (!singleton.has(info.module)) {
			singleton.set(info.module, { ps: null, view: null });
		}
		let map = singleton.get(info.module);
		if (!map.view) {
			if (!map.ps) {
				map.ps = Promise.resolve().then(() => {
					if (this.name === 'root') {
						return StaticViewGroup.addChild(viewClass, option, root);
					} else {
						return StaticViewGroup.addChild(`${this.name}/${info.module}`, option, root);
					}
				}).then(view => {
					map.view = view;
					map.ps = null;
					return view;
				});
			}
			return map.ps;
		} else {
			return Promise.resolve(map.view);
		}
	}

	appendChild(viewClass, option) {
		let root = this.getRoot();
		let name = 'global-blank-staticviewgroup';
		if (!root.context._singleton) {
			root.context._singleton = new Map();
		}
		let singleton = root.context._singleton;
		if (!singleton.has(name)) {
			singleton.set(name, { ps: null, view: null });
		}
		let map = singleton.get(name);
		if (!map.view) {
			if (!map.ps) {
				map.ps = Promise.resolve().then(() => {
					return StaticViewGroup.addChild(StaticViewGroup, {}, root);
				}).then(view => {
					map.view = view;
					map.ps = null;
					return view;
				});
			}
			return map.ps.then(view => {
				return view.addChild(viewClass, option);
			});
		} else {
			return map.view.addChild(viewClass, option);
		}
	}

	ready() {
		return this.loader.getManifest(this.config).then(manifest => {
			if (!manifest || !manifest.map) {
				return this.loader.getRemoteMainifest(this.config).then(codeMap => {
					Object.assign(this.config.codeMap, codeMap);
					return this.loader.setManifest(codeMap).then(() => codeMap);
				});
			} else {
				Object.assign(this.config.codeMap, manifest);
				return manifest;
			}
		}).then(info => {
			let { root, develop, map } = info;
			Object.assign(this._config, { root, develop, sourceMap: map });
			env.develop = info.develop;
		});
	}

	boot({ parameter = {}, container, dom, parent = null, useProps, id = '', inited = true, entry = '', name, _viewId } = {}) {
		return this.ready().then(() => {
			Object.assign(this._config, { parameter });
			let root = entry || this._config.root;
			if (root.indexOf('.') === -1) {
				root = root + '.js';
			}
			if (root.indexOf("./") === 0) {
				root = root.substring(2);
			}
			if (root.indexOf("/") === 0) {
				root = root.substring(1);
			}
			let ps = Promise.resolve();
			if (this._config.hooker) {
				ps = ps.then(() => {
					return this.loader.getModule(this._config.hooker).then(info => {
						this._hooks = info;
					}).then(() => {
						if (parent) {
							this._streamManager.setParent(parent.context._streamManager);
						}
					}).then(() => {
						return this._invokeHook('oncontextcreated', this);
					});
				});
			}
			return ps.then(() => {
				return this.loader.getModule(root).then(rootClass => {
					if (rootClass && Metadata.getMetadataExtends("view", rootClass.prototype)) {
						return this.getRootView({ rootClass, dom, container, parameter, parent, useProps, id, name, _viewId, inited }).then(view => {
							this._root = view;
							return this._root;
						});
					} else {
						console.error("root class can not find");
						return Promise.reject();
					}
				});
			});
		});
	}

	snapshot() {
		setTimeout(() => {
			this._manager && this._manager.snapshot();
		});
	}

	getEntry(name) {
		let { app, path } = this._getEntryPath(name);
		let context = this.manager.getContext(app);
		if (context) {
			if (path.split('.').pop() !== 'js') {
				path = path + '.js';
			}
			return context.ready().then(() => {
				return context.loader.getModule(path);
			});
		} else {
			return Promise.resolve(null);
		}
	}

	checkEntry(name) {
		return this.getEntry(name).then(a => !!a);
	}

	isEntryCached(name) {
		let { app, path } = this._getEntryPath(name);
		let context = this.manager.getContext(app);
		if (context) {
			if (path.split('.').pop() !== 'js') {
				path = path + '.js';
			}
			return context.ready().then(() => {
				return context.loader.isCached(path);
			});
		} else {
			return Promise.resolve(false);
		}
	}
}

module.exports = BaseContext;